/* gtktransition.c
 *
 * Copyright (C) 2007 Andre Moreira Magalhaes <andrunko@gmail.com>
 * Copyright (C) 2007 Renato Araujo Oliveira Filho <renatox@gmail.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "gtktransition.h"

typedef struct _GtkTransitionPrivate   GtkTransitionPrivate;
typedef struct _GtkTransitionStateInfo GtkTransitionStateInfo;

struct _GtkTransitionPrivate
{
  GSList *states;
};

struct _GtkTransitionStateInfo
{
  GtkTransitionState *parent_state;
  gdouble progress;
};

#define GTK_TRANSITION_GET_PRIVATE(o) \
  (G_TYPE_INSTANCE_GET_PRIVATE ((o), GTK_TYPE_TRANSITION, GtkTransitionPrivate))

static void     gtk_transition_class_init (GtkTransitionClass *klass);
static void     gtk_transition_init       (GtkTransition      *transition);
static void     gtk_transition_dispose    (GObject            *object);
static void     gtk_transition_frame      (GtkTimeline        *timeline,
                                           gdouble             progress);

G_DEFINE_TYPE (GtkTransition, gtk_transition, GTK_TYPE_TIMELINE);

static GQuark quark_state_info = 0;

static void
gtk_transition_class_init (GtkTransitionClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkTimelineClass *timeline_class = GTK_TIMELINE_CLASS (klass);

  g_type_class_add_private (klass, sizeof (GtkTransitionPrivate));

  object_class->dispose = gtk_transition_dispose;

  timeline_class->frame = gtk_transition_frame;

  quark_state_info = g_quark_from_string ("gtk-transition-state-info");
}

static void
gtk_transition_init (GtkTransition *transition)
{
}

static void
gtk_transition_dispose (GObject *object)
{
  GtkTransitionPrivate *priv;
  GSList *states;
  GtkTransitionState *state;

  priv = GTK_TRANSITION_GET_PRIVATE (object);

  for (states = priv->states; states; states = states->next)
    {
      state = (GtkTransitionState *) states->data;
      g_object_set_qdata (G_OBJECT (state),
                          quark_state_info,
                          NULL);
      g_object_unref (G_OBJECT (state));
    }
  g_slist_free (priv->states);

  G_OBJECT_CLASS (gtk_transition_parent_class)->dispose (object);
}

/**
 * gtk_transition_new:
 * @duration: duration in milliseconds for the transition
 * @returns: a newly created #GtkTransition
 *
 * Creates a new #GtkTransition with the specified duration.
 **/
GtkTransition *
gtk_transition_new (guint duration)
{
  return g_object_new (GTK_TYPE_TRANSITION,
                       "duration", duration,
                       NULL);
}

/**
 * gtk_transition_get_states:
 * @transition: a #GtkTransition
 * @returns: a newly allocated list of the states added to @transition
 *
 * Gets the list of states added to @transition.
 *
 * The returned list must be freed, but the states in the list don't.
 **/
GSList *
gtk_transition_get_states (GtkTransition *transition)
{
  GtkTransitionPrivate *priv;

  g_return_val_if_fail (GTK_IS_TRANSITION (transition), NULL);

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  if (priv->states)
    return g_slist_copy (priv->states);

  return NULL;
}

/**
 * gtk_transition_get_state:
 * @transition: a #GtkTransition
 * @object: the #GObject in which the state applies to
 * @parent_state: the parent #GtkTransitionState of the state, or NULL if none
 * @progress: the progress in which the state will be finished (between 0.0 and 1.0 inclusive)
 * @returns: pointer to a #GtkTransitionState or NULL if not found
 *
 * Gets a state added to @transition that applies to @object,
 * has the parent state set to @parent_state and will finish
 * on @progress.
 *
 * The returned state does not have a reference added, so you
 * do not need to unref it.
 **/
GtkTransitionState *
gtk_transition_get_state (GtkTransition      *transition,
                          GObject            *object,
                          GtkTransitionState *parent_state,
                          gdouble             progress)
{
  GtkTransitionPrivate *priv;
  GSList *states;
  GtkTransitionState *state;
  GtkTransitionStateInfo *state_info;
  GObject *state_object;

  g_return_val_if_fail (GTK_IS_TRANSITION (transition), NULL);
  g_return_val_if_fail (G_IS_OBJECT (object), NULL);
  g_return_val_if_fail (parent_state == NULL || GTK_IS_TRANSITION_STATE (parent_state), NULL);
  g_return_val_if_fail (progress >= 0.0 && progress <= 1.0, NULL);

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  for (states = priv->states; states; states = states->next)
    {
      state = (GtkTransitionState *) states->data;
      state_info = g_object_get_qdata (G_OBJECT (state),
                                       quark_state_info);
      state_object = gtk_transition_state_get_object (state);
      if (state_object == object &&
          state_info->parent_state == parent_state &&
          state_info->progress == progress)
        return state;
    }

  return NULL;
}

/**
 * gtk_transition_get_state_parent_state:
 * @transition: a #GtkTransition
 * @state: a #GtkTransitionState added to @transition
 * @returns: @state parent @GtkTransitionState or NULL if @state does not have a parent state set.
 *
 * Gets the parent #GtkTransitionState of @state.
 *
 * The returned state does not have a reference added, so you
 * do not need to unref it.
 *
 * If state is not added to @transition, NULL will be returned.
 **/
GtkTransitionState *
gtk_transition_get_state_parent_state (GtkTransition      *transition,
                                       GtkTransitionState *state)
{
  GtkTransitionPrivate *priv;
  GSList *states;
  GtkTransitionStateInfo *state_info;

  g_return_val_if_fail (GTK_IS_TRANSITION (transition), NULL);
  g_return_val_if_fail (GTK_IS_TRANSITION_STATE (state), NULL);

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  for (states = priv->states; states; states = states->next)
    {
      if (state == (GtkTransitionState *) states->data)
        {
          state_info = g_object_get_qdata (G_OBJECT (state),
                                           quark_state_info);
          return state_info->parent_state;
        }
    }

  return NULL;
}

/**
 * gtk_transition_get_state_progress:
 * @transition: a #GtkTransition
 * @state: a #GtkTransitionState added to @transition
 * @returns: the progress in which @state will be finished.
 *
 * Gets the progress in which @state will be finished.
 *
 * If state is not added to @transition, 0 will be returned.
 **/
gdouble
gtk_transition_get_state_progress (GtkTransition      *transition,
                                   GtkTransitionState *state)
{
  GtkTransitionPrivate *priv;
  GSList *states;
  GtkTransitionStateInfo *state_info;

  g_return_val_if_fail (GTK_IS_TRANSITION (transition), 0);
  g_return_val_if_fail (GTK_IS_TRANSITION_STATE (state), 0);

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  for (states = priv->states; states; states = states->next)
    {
      if (state == (GtkTransitionState *) states->data)
        {
          state_info = g_object_get_qdata (G_OBJECT (state),
                                           quark_state_info);
          return state_info->progress;
        }
    }

  return 0;
}

/**
 * gtk_transition_has_state:
 * @transition: a #GtkTransition
 * @state: a #GtkTransitionState added to @transition
 * @returns: TRUE if @state is added to @transition, FALSE otherwise.
 *
 * Checks if @state is added to @transition.
 **/
gboolean
gtk_transition_has_state (GtkTransition      *transition,
                          GtkTransitionState *state)
{
  GtkTransitionPrivate *priv;
  GSList *states;

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  for (states = priv->states; states; states = states->next)
    {
      if (state == (GtkTransitionState *) states->data)
        return TRUE;
    }

  return FALSE;
}

/**
 * gtk_transition_add_state:
 * @transition: a #GtkTransition
 * @state: the #GtkTansitionState to be added
 * @parent_state: the parent #GtkTransitionState of @state or NULL if none
 * @progress: the progress in which the state will be finished (between 0.0 and 1.0 inclusive)
 *
 * Adds @state to @transition. If @parent_state is not NULL
 * @state will start execution after @parent_state execution has
 * finished.
 *
 * Note that if @parent_state is set @progress must be bigger
 * than @parent_state progress.
 **/
void
gtk_transition_add_state (GtkTransition      *transition,
                          GtkTransitionState *state,
                          GtkTransitionState *parent_state,
                          gdouble             progress)
{
  GtkTransitionPrivate *priv;
  GtkTransitionStateInfo *state_info, *parent_state_info;

  g_return_if_fail (GTK_IS_TRANSITION (transition));
  g_return_if_fail (GTK_IS_TRANSITION_STATE (state));
  g_return_if_fail (parent_state == NULL || GTK_IS_TRANSITION_STATE (parent_state));
  g_return_if_fail (progress >= 0.0 && progress <= 1.0);

  /* check if state is already added to this transition */
  if (gtk_transition_has_state (transition, state))
    {
      g_debug ("%s: state already added to transition");
      return;
    }

  /* check if we have the parent state added to this transition */
  if (parent_state)
    {
      if (!gtk_transition_has_state (transition, parent_state))
        {
          g_warning ("%s: parent state should be added to transition first",
                     G_STRFUNC);
          return;
        }

      parent_state_info = g_object_get_qdata (G_OBJECT (parent_state),
                                              quark_state_info);
      if (parent_state_info->progress >= progress)
        {
          g_warning ("%s: cannot add state with progress smaller than parent state progress",
                     G_STRFUNC);
          return;
        }
    }

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  state_info = g_new (GtkTransitionStateInfo, 1);
  state_info->parent_state = parent_state;
  state_info->progress = progress;
  g_object_set_qdata_full (G_OBJECT (state),
                           quark_state_info,
                           state_info,
                           g_free);

  priv->states = g_slist_append (priv->states,
                                 g_object_ref (state));
}

/**
 * gtk_transition_remove_state:
 * @transition: a #GtkTransition
 * @state: the #GtkTansitionState to be removed
 *
 * Removes @state from @transition.
 **/
void
gtk_transition_remove_state (GtkTransition      *transition,
                             GtkTransitionState *state)
{
  GtkTransitionPrivate *priv;
  GSList *states;

  g_return_if_fail (GTK_IS_TRANSITION (transition));
  g_return_if_fail (GTK_IS_TRANSITION_STATE (state));

  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  for (states = priv->states; states; states = states->next)
    {
      if (state == (GtkTransitionState *) states->data)
        {
           g_object_set_qdata (G_OBJECT (state),
                               quark_state_info,
                               NULL);
           g_object_unref (G_OBJECT (state));
           priv->states = g_slist_remove (priv->states, state);
           return;
        }
    }
}

static void
gtk_transition_frame (GtkTimeline *timeline,
                      gdouble      progress)
{
  GtkTransition *transition;
  GtkTransitionPrivate *priv;
  GSList *states;
  GtkTransitionState *state, *parent_state;
  GtkTransitionStateInfo *state_info, *parent_state_info;
  gdouble state_progress, parent_progress, next_progress;

  transition = GTK_TRANSITION (timeline);
  priv = GTK_TRANSITION_GET_PRIVATE (transition);

  if (!priv->states)
    return;

  for (states = priv->states; states; states = states->next)
    {
      state = (GtkTransitionState *) states->data;
      state_info = g_object_get_qdata (G_OBJECT (state),
                                       quark_state_info);
      if (state_info->progress >= progress)
        {
          parent_state = state_info->parent_state;
          if (parent_state)
            {
              parent_state_info = g_object_get_qdata (G_OBJECT (parent_state),
                                                      quark_state_info);
              /* ignore state if parent has not finished yet */
              if (parent_state_info->progress >= progress)
                continue;
              parent_progress = parent_state_info->progress;
            }
          else
            parent_progress = 0;

          next_progress = state_info->progress;

          state_progress = (progress - parent_progress) / (next_progress - parent_progress);
          gtk_transition_state_set_progress (state, state_progress);
        }
    }
}
